/* License Notice:
**
** This program is free software: you can redistribute it and/or modify
**    it under the terms of the GNU General Public License as published by
**    the Free Software Foundation, either version 3 of the License, or
**    (at your option) any later version.
** This program is distributed in the hope that it will be useful,
**   but WITHOUT ANY WARRANTY; without even the implied warranty of
**   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
**   GNU General Public License for more details.
** You should have received a copy of the GNU General Public License
**   along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

/**
 * @file properties.hpp
 * @author TooOld2Rock'nRoll
 * @date 2024/10/17
 * @brief Handles properties and behavior for ArcadeFighter objects.
 *
 * @bug Update() should not exist, but if we uncomment the code inside isDirty(), sprites blink when changing animations.
 * @todo Change class names to [name]Property, current names are confusing.
 * @todo Add a method to Color that accepts a series of floats.
 * @todo volume (audio) property.
 */
#ifndef _PROPERTIES_HPP_
#define _PROPERTIES_HPP_

/*---- Includes ----*/
#include <atomic>
#include <glm/fwd.hpp>
#include <memory>

#include <glm/glm.hpp>


/*---- Class Declaration ----*/
/**
 * @brief Interface with common features of object properties.
 * @remark It's not elegant to implement this much code in the header, but we can't have templates in source files for
 *           libraries and all object properties will share a lot of code!
 *
 * @tparam P - the property kind.
 */
template <typename P>
class AbstractProperty
{
    private:


    protected:
        std::atomic<bool> b_dirty_flag = true; ///<If the property value was changed.

        P _property; ///<The actual property the class is regulating.
        std::weak_ptr<P> p_ext_reference; ///<Pointer to an external reference.


    public:
        /** @brief Default Constructor. */
        AbstractProperty () {  }
        /** @brief Property reference Constructor. */
        AbstractProperty (const P &prop) { this->set (prop); }
        /** @brief External reference Constructor. */
        AbstractProperty (const std::shared_ptr<P> &external) { this->entangle (external); }
        /** @brief Destructor. */
        virtual ~AbstractProperty () {  }

        /** @brief Copy assignment. */
        AbstractProperty& operator= (const P &prop) { this->set (prop); }

        /** @brief Copy assignment. */
        AbstractProperty& operator= (const AbstractProperty &other)
        {
            // Guard self assignment
            if (this == &other)
                return *this;

            this->set (other._property);
            this->entangle (other.p_ext_reference.lock ());
        }//end operator=

        /** @brief Tells if the property was modified since last reset. */
        bool isDirty () const
        {
            // if (this->isEntangled ())
            // {
            //     std::shared_ptr<P> temp_sp = this->p_ext_reference.lock ();

            //     if (*temp_sp != this->_property)
            //     {
            //         this->_property = *temp_sp;
            //         this->b_dirty_flag = true;
            //     }//end if (*temp_sp != this->_property)
            // }//end if (this->isEntangled ())

            return this->b_dirty_flag;
        }//end isDirty ()

        /** @brief Resets to false the object dirty flag. */
        void resetFlag () { this->b_dirty_flag = false; }

        /** @brief Sets the object's absolute property. */
        void set (const P &prop)
        {
            if (prop == this->_property)
                return; //avoid rebuilding model

            if (!this->p_ext_reference.expired ())
                *this->p_ext_reference.lock() = prop;

            this->b_dirty_flag = true;
            this->_property = prop;
        }//end set ()

        /** @brief Gets the object's property value. */
        const P & get () const { return this->_property; }


        /** @brief Returns the current state of the property entanglement. */
        bool isEntangled () const { return !this->p_ext_reference.expired (); }

        /** @brief Entangles this object's property with an external property reference.
         * @remarks This is a useful behavior to apply to things like sprite batches, objects closely related that share
         *            the same property but with different states.
         */
        void entangle (std::shared_ptr<P> prop) { this->p_ext_reference = prop; }

        /** @brief Gets a reference to the entangled property reference. */
        std::weak_ptr<P> getEntangledReference () { return this->p_ext_reference; }

        /** @brief Updates the Property value to its external reference. */
        void update ()
        {
            std::shared_ptr<P> temp_sp;

            if (!this->isEntangled ())
                return;

            temp_sp = this->p_ext_reference.lock ();
            if (*temp_sp != this->_property)
            {
                this->_property = *temp_sp;
                this->b_dirty_flag = true;
            }//end if (*temp_sp != this->_property)
        }//end update ()

        /** @brief Object property will go back to being locally regulated. */
        void unentangle () { this->p_ext_reference.reset (); }
};//END AbstractProperty


/**
 * @brief Regulates the state and rules to handle an object position.
 */
class Position : public AbstractProperty<glm::vec3>
{
    private:


    protected:


    public:
        /** @brief Default Constructor. */
        Position () : AbstractProperty(glm::vec3 (0.f)) {  }
        /** @brief xyz float Constructor. */
        Position (float x, float y, float z = 0.f) : AbstractProperty(glm::vec3(x, y, z)) {  }

        //because we are overloading the set method!
        using AbstractProperty::set;
        /** @brief Sets xyz position. */
        void set (float x, float y, float z = 0.f) { this->AbstractProperty::set (glm::vec3 (x, y, z)); }

        void translate (const glm::vec3 &trans);
        /** @brief Adds or subtracts from the object's current position. */
        void translate (float x, float y, float z = 0.f) { this->translate (glm::vec3 (x, y, z)); }
};//END Position

/**
 * @brief Regulates the state and rules to handle an object angle in 2D space.
 */
class Angle2D : public AbstractProperty<float>
{
    public:
        /** @brief Which point to rotate object at? (bit mask) */
        enum rotate_at
        {
            TOP     = 0b000001,///< Top/Middle
            BOTTOM  = 0b000010,///< Bottom/Middle
            LEFT    = 0b000100,///< Left/Middle
            RIGHT   = 0b001000,///< Right/Middle
            CENTER  = 0b010000 ///< Center [default]
        };//end rotate_at


    private:


    protected:
        glm::vec2 v2_rotation_point = glm::vec2 (0.5); ///<The exact point of rotation, if defined by the user.


    public:
        /** @brief Default Constructor. */
        Angle2D () : AbstractProperty(0.f) {  }

        void setRotationPoint (Angle2D::rotate_at at);
        /** @brief Sets a specific point to rotate/mirror the object at.
         * @param point - [x,y] point between (0.0 >= 1.0)
         * @remark The reference point may be outside the object. */
        void setRotationPoint (const glm::vec2 &point) { this->v2_rotation_point = point; }
        /** @brief Gets the object rotation point. */
        const glm::vec2 & getRotationPoint () { return this->v2_rotation_point; }

        void rotate (float r);
};//END Angle2D

/**
 * @brief Regulates the state and rules to handle an object's color.
 */
class Color : public AbstractProperty<glm::vec4>
{
    private:


    protected:


    public:
        /** @brief Default Constructor. */
        Color () : AbstractProperty(glm::vec4 (1.f)) {  }
};//END Angle2D

/**
 * @brief Regulates the state and rules to handle an object size.
 */
class Size : public AbstractProperty<glm::vec3>
{
    private:


    protected:
        float f_scale_factor = 1.f; ///<To be applied over the object size independent of any scale/resize operation.


    public:
        /** @brief Default Constructor. */
        Size () : AbstractProperty(glm::vec3 (0.f)) {  }
        /** @brief xyz float Constructor. */
        Size (float x, float y, float z = 0.f) : AbstractProperty(glm::vec3(x, y, z)) {  }

        //because we are overloading the set method!
        using AbstractProperty::set;
        /** @brief Sets xyz size. */
        void set (float x, float y, float z = 0.f) { this->AbstractProperty::set (glm::vec3 (x, y, z)); }

        void setScaleFactor (float s_factor);
        /** @brief Gets the object the scale factor. */
        float getScaleFactor () const {return this->f_scale_factor;}
        /** @brief Gets the object's real size multiplying it by the scale factor. */
        glm::vec3 getScaled () const { return this->_property * this->f_scale_factor; }

        /** @brief Scales the object size uniformly.
         * @remark If you want a scale to be applied independently of the actual size, use setScaleFactor() instead.
         * The scale is applied over the object's current size and overridden by set(),
         *    so remember to set a proper size to the sprite before invoking this method and
         *    remember that animation operations may change its size on every frame update.<br>
         * This is probably what you want for temporary visual effects of enlarging and shrinking the object. */
        void scale (float s) { this->set (this->_property * s); }

        /** @brief Scales the object size.
         * @remark If you want a scale to be applied independently of the actual size, use setScaleFactor() instead.
         * The scale is applied over the object's current size and overridden by set(),
         *    so remember to set a proper size to the sprite before invoking this method and
         *    remember that animation operations may change its size on every frame update.<br>
         * This is probably what you want for temporary visual effects of enlarging and shrinking the object. */
        void scale (float x, float y, float z = 1.f)
        { this->set (x * this->_property.x, y * this->_property.y, z * this->_property.z); }

        /** @brief Adds or subtracts from the object's current size. */
        void resize (const glm::vec3 &re) { this->set (this->_property + re); }
        /** @brief Adds or subtracts from the object's current size. */
        void resize (float x, float y, float z = 0.f) { this->resize (glm::vec3 (x, y, z)); }
};//END Size

#endif //_PROPERTIES_HPP_

